package com.example.demo.util;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.example.demo.config.ConstantConfig;
import com.example.demo.enums.StatusCodeEnum;
import com.example.demo.exception.CustomException;
import io.minio.*;
import io.minio.messages.*;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;


/**
 * @description：操作minio服务文件的工具类.
 * @explain：文件存储分为对象存储和磁盘存储，minio是对象存储服务(oss)，像我们将文件保存到本地服务就是磁盘存储
 * @author：lw
 * @create_time：2024/1/18
 **/


public class MinioFileUtil {
    // 这里的fileName和folder 一律从桶下开始算，比如test桶下有个文件 http://localhost:9000/test/13123/1.jpg ，则filename是 13123/1.jpg

    private static final Map<String, List<String>> hashToChunkFilesMap=new ConcurrentHashMap<>();     // 大文件上传使用
    private static final Map<String, InputStream> fileNameToChunkFileMap=new ConcurrentHashMap<>();  // 大文件下载使用

    private  static final MinioClient minioClient = MinioClient.builder()
            .endpoint(ConstantConfig.minio_serverUrl)
            .credentials(ConstantConfig.minio_username,ConstantConfig.minio_password)
            .build();

    public static String getFileNameByFullFileUrl(String fullFileUrl){
       return fullFileUrl.split(ConstantConfig.minio_serverUrl+"/"+ConstantConfig.minio_buketName)[1];
    }

    public static String upload(MultipartFile file,String fileType) {     // 多文件上传则循环调用该方法即可
        try {
            // 创建bucket
            boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(ConstantConfig.minio_buketName).build());
            if (!found) {
                // 没有就创建一个新的文件桶.
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(ConstantConfig.minio_buketName).build());
            }
//            else {
//                System.out.println("bucket "+ ConstantConfig.minio_buketName + " has exist");
//            }

            //获取上传文件名称
            // 1 每个上传文件名称唯一的   uuid_fileName
            // 2 根据当前日期对上传文件进行文件夹分组 yyyyMMdd
            // 3 根据文件的类型对文件进行文件夹分组
            String dateDir = DateUtil.format(new Date(), "yyyyMMdd") + "/" + fileType;
            String filename = dateDir+"/" +IdUtil.simpleUUID() + ConstantConfig.fileNameSplitSymbol + file.getOriginalFilename();
            // 文件上传
            minioClient.putObject(
                    PutObjectArgs.builder().bucket(ConstantConfig.minio_buketName)
                            .object(filename)    // 上传同名对象，minio采用覆盖模式
                            //.stream(file.getInputStream(), file.getSize(), -1)   // 最后一个参数，限制上传文件的大小，-1表示5mb，最大为-10000为5T
                            .stream(file.getInputStream(), file.getSize(), -2)
                            .build());

            //返回上传文件的路径
            return ConstantConfig.minio_serverUrl+"/"+ConstantConfig.minio_buketName+"/"+filename;
        } catch (Exception e) {
            e.printStackTrace();
            throw new CustomException(StatusCodeEnum.upload_fail);
        }
    }

    public static String uploadChunk(MultipartFile partFile,String md5Hash,String fileName,Integer total,Integer current) {
        if(current.equals(1)){
            String bigFileUrl=bigFileExist(md5Hash);
            // 大文件存在则不需要上传,直接返回大文件路径
            if(bigFileUrl!=null) return  ConstantConfig.minio_serverUrl+"/"+ConstantConfig.minio_buketName+"/"+bigFileUrl;
        }
        String dir_upload =ConstantConfig.minio_temp_folder +"/" + md5Hash;
        String filename_upload = dir_upload +"/"+ current +ConstantConfig.fileNameSplitSymbol+fileName;
        // 断点续传，上传过的分片直接不上传并返回切片路径
        if(getInfo(filename_upload)!=null) {
            if(current.equals(total)) return mergeHandle(dir_upload,fileName,md5Hash,total);
            return ConstantConfig.minio_serverUrl+"/"+ConstantConfig.minio_buketName+"/"+filename_upload;
        }
        try {
            boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(ConstantConfig.minio_buketName).build());
            if (!found)  minioClient.makeBucket(MakeBucketArgs.builder().bucket(ConstantConfig.minio_buketName).build());
            int s=(int)Math.ceil(partFile.getSize() / (5 * 1024 * 1024));
            Long maxSize=-1L * (s==0?1:s);
            Map<String,String> info=new HashMap<>();
            info.put("total",total.toString());
            info.put("current",current.toString());
            minioClient.putObject(
                    PutObjectArgs.builder().bucket(ConstantConfig.minio_buketName)
                            .object(filename_upload)
                            .userMetadata(info)       // 该信息可通过 getFileInfo 获取
                            .stream(partFile.getInputStream(), partFile.getSize(),maxSize )
                            .build());

            if(hashToChunkFilesMap.get(md5Hash)==null) hashToChunkFilesMap.put(md5Hash,new ArrayList<>());
            hashToChunkFilesMap.get(md5Hash).add(filename_upload);

            if(total.equals(current)){   // 所有分片上传完成
              return mergeHandle(dir_upload,fileName,md5Hash,total);
            }
            return ConstantConfig.minio_serverUrl+"/"+ConstantConfig.minio_buketName+"/"+filename_upload;  // 返回分片路径
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("MinioFileUtil.java 第116行："+e.getMessage());
        }
    }

    public static String mergeHandle(String dir_upload,String fileName,String md5Hash,Integer total){
        List<String> allChunkNameList=getListInfo(dir_upload).stream().map(v->v.objectName()).toList();
        // 当文件名更改后再续传会导致重复合并，filter是防止这种情况
        List<String> chunkNameList=allChunkNameList.stream().filter(v->v.contains(fileName)).toList();

        if(chunkNameList.size()!=total) throw new RuntimeException("文件分片总量不对，文件数量："+ chunkNameList.size()+"您告知的数量："+ total);
        List<String> mutableList = new ArrayList<>(chunkNameList);    // 因为getListInf得到的是一个只读集合，因此先要把集合转成可写的才行，否则sort方法会报错
        mutableList.sort((pre,next)->{
            // 获取切片编号，根据编号做升序
            String preNo=pre.replace(dir_upload+"/","").split(ConstantConfig.fileNameSplitSymbol)[0];
            String nextNo=next.replace(dir_upload+"/","").split(ConstantConfig.fileNameSplitSymbol)[0];
            return  Integer.parseInt(preNo) -Integer.parseInt(nextNo);
        });
        // 生成简单存储路径，方便后续查找，后续上传大文件就可以根据md5值看看文件服务里有没有该文件，有的话就不需要再上传
        String filename_new = ConstantConfig.minio_bigFile_folder+"/" +md5Hash+"/"+fileName;
        mergeChunks(mutableList,filename_new);
        removeList(allChunkNameList);
        hashToChunkFilesMap.remove(md5Hash);
        return ConstantConfig.minio_serverUrl+"/"+ConstantConfig.minio_buketName+"/"+filename_new;  //返回合成的大文件路径
    }

    public static void download(String fileName, HttpServletResponse response) {
        if (StrUtil.isBlank(fileName))   throw  new CustomException(StatusCodeEnum.fileName_null);

        try(
            // 获取文件对象
            InputStream inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(ConstantConfig.minio_buketName).object(fileName).build());
            OutputStream outputStream = response.getOutputStream();
        )
        {
//            String really_fileName=URLEncoder.encode(fileName.substring(fileName.lastIndexOf("/") + 1), StandardCharsets.UTF_8);
            String really_fileName=URLEncoder.encode(fileName.split(ConstantConfig.fileNameSplitSymbol)[1], StandardCharsets.UTF_8);
            response.setHeader("Content-Disposition", "attachment;filename=" +really_fileName);
            response.setContentType("application/octet-stream");
            response.setCharacterEncoding("UTF-8");
            byte[] buf = new byte[1024];      // 1k1k的传
            int length = 0;
            // 输出文件，这里也可以使用io工具把输入流的东西copy到输出流
            while ((length = inputStream.read(buf)) > 0) {
                outputStream.write(buf, 0, length);
            }
        } catch (Throwable e) {
            try(OutputStream outPs=response.getOutputStream()){
                response.setHeader("Content-type", "text/html;charset=UTF-8");
                String data = "文件下载失败," + e.getMessage();
                outPs.write(data.getBytes(StandardCharsets.UTF_8));  // 字节方式写入输出流
                //PrintWriter writer = response.getWriter()
                //writer.print(JSON.toJSONString(data));           // json方式写入到输出流
            }
            catch(Throwable ex){
                throw new RuntimeException(ex.getMessage());
            }

        }
    }

    public static ResponseEntity<InputStreamResource> download(String fileName) {

        if (StrUtil.isBlank(fileName))  throw  new CustomException(StatusCodeEnum.fileName_null);

        try(InputStream inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(ConstantConfig.minio_buketName).object(fileName).build())){
            // 设置响应头
            HttpHeaders headers = new HttpHeaders();
            headers.add("Content-Disposition", String.format("attachment; filename=\"%s\"",
                    URLEncoder.encode(fileName.substring(fileName.lastIndexOf("/") + 1), StandardCharsets.UTF_8)));

            return ResponseEntity
                    .ok()
                    .headers(headers)
                    .contentType(MediaType.parseMediaType("application/octet-stream"))
                    .body(new InputStreamResource(inputStream));
        }catch (Exception e){
            throw new RuntimeException(e.getMessage());
        }

    }

    public static boolean downloadChunk(String fileName,Integer current,HttpServletResponse response) throws IOException {
        InputStream inputStream = fileNameToChunkFileMap.get(fileName);
        OutputStream outputStream = response.getOutputStream();
        if (inputStream == null) {
            try {
                inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(ConstantConfig.minio_buketName).object(fileName).build());
                fileNameToChunkFileMap.put(fileName, inputStream);
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage());
            }
        }
        int size = 1024 * 1024 * 10;
        byte[] buf = new byte[size];   // 10mb
        int len = inputStream.read(buf, (current - 1) * size, size);
        if(len==0){  // 读取完毕。控制层调用该方法时，如果得到false，需要告知前端文件传输完毕
            inputStream.close();
            outputStream.close();
            return false;
        }
        outputStream.write(buf, 0, len);
        inputStream.close();
        outputStream.close();
        return true;
    }

    public static void remove(String fileName){
        try{
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(ConstantConfig.minio_buketName).object(fileName).build());
        } catch (Exception e){
            throw new RuntimeException("删除失败"+e.getMessage());
        }
    }

    public static List<DeleteError> removeList(List<String> fileNameList){
        List<DeleteError> list=new ArrayList<>();
        List<DeleteObject> deleteObjectList = new ArrayList<>();
        fileNameList.forEach(v->deleteObjectList.add(new DeleteObject(v)));
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder()
                .bucket(ConstantConfig.minio_buketName).objects(deleteObjectList).build());
        for (Result<DeleteError> result : results) {
            try {
               list.add(result.get());
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return list;
    }

    public static boolean  mergeChunks(List<String> chunkFileNameList,String newFileName){
        List<ComposeSource> sourceObjectList = new ArrayList<>();

        chunkFileNameList.forEach(v->{
            sourceObjectList.add(ComposeSource.builder()
                    .bucket(ConstantConfig.minio_buketName)
                    .object(v)
                    .build());
        });

        try{    // 注意：minio合并文件时，单个文件最小为5mb
            minioClient.composeObject(
                    ComposeObjectArgs.builder()
                            .bucket(ConstantConfig.minio_buketName)
                            .object(newFileName)
                            .sources(sourceObjectList)
                            .build());
        }catch (Exception e){
            throw new RuntimeException("MinioFileUtil.java 第265行： "+e.getMessage());
        }

        return true ;
    }

    public static String bigFileExist(String md5Hash){
        String folder=ConstantConfig.minio_bigFile_folder+"/" +md5Hash;
        List<Item> list  =  getListInfo(folder);
        if(list.size()==0) return  null;
        return folder +"/"+ list.get(0).objectName();
    }

    public static StatObjectResponse  getInfo(String fileName){
        try {
           StatObjectResponse statObjectResponse = minioClient.statObject(StatObjectArgs.builder().bucket(ConstantConfig.minio_buketName).object(fileName).build());
           return  statObjectResponse;
        }catch (Exception e){
            return  null;
            //throw new RuntimeException(e.getMessage());
        }
    }

    public static List<Item>  getListInfo(String folder){
        List<Item> list =new ArrayList<>();
        // recursive(false)表示不递归获取，只获取下一层，为ture则根据条件递归获取。这里如果不调prefix就是直接在同根部找了
        Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder()
                .bucket(ConstantConfig.minio_buketName).prefix(folder + "/").recursive(false).build());
        if(results!=null)
        for (Result<Item> result : results) {
            try{list.add(result.get());
            }catch (Exception e){e.printStackTrace();}
        }
        return  list;
    }

    public static boolean setExpire(String fileName,Long hour,Long minute){
        // 设置文件的保留时间为当前时间加上 hour 小时，minute分钟，时间精确到秒。 加是plus，减是min
        ZonedDateTime retentionUntil = ZonedDateTime.now(Time.UTC).plusHours(hour).plusMinutes(minute).withNano(0);
        Retention config = new Retention(RetentionMode.COMPLIANCE, retentionUntil);
        try{
            minioClient.setObjectRetention(SetObjectRetentionArgs.builder()
                    .bucket(ConstantConfig.minio_buketName).object(fileName).config(config).build());
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
        return true ;
    }

    private void mergeInputStream(List<InputStream> inputStreamList) throws IOException {
        // 如果觉得 minio 默认>5mb 合并限制太大，可以自行合并流来实现文件组装
        // 将多个InputStream放入Vector容器中
        Vector<InputStream> inputStreams = new Vector<>();
        inputStreamList.forEach(v->inputStreams.add(v));

        // 使用Enumeration包装Vector，以便SequenceInputStream使用
        Enumeration<InputStream> enumerationOfStreams = Collections.enumeration(inputStreams);

        // 创建一个SequenceInputStream来合并所有输入流
        InputStream mergedStream = new SequenceInputStream(enumerationOfStreams);

        // 现在你可以像操作单个InputStream一样读取mergedStream中的数据
        byte[] buffer = new byte[1024];
        int readBytes;
        while ((readBytes = mergedStream.read(buffer)) != -1) {
            // 把所有input流写到一个fileout流里生成一个临时文件，然后通过临时文件获取input流即可.
        }

        // 别忘了关闭所有输入流
        mergedStream.close();
        inputStreamList.forEach(v-> {
            try {
                v.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }

    public static InputStream mergeInputStreams(List<InputStream> inputStreams){
        Enumeration<InputStream> en = Collections.enumeration(inputStreams);
        InputStream inputStream=new SequenceInputStream(en);
        inputStreams.forEach(v->{
            try {
                v.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        return  inputStream;
    }
}

    /*
    window启动minio服务 -> cmd窗口执行指令 ：minio.exe server 文件的存储路径（绝对路径）
   docker启动 ->
   docker run \
	# 映射端口
	-p 9000:9000 -p 9001:9001 \
	# 容器名
	--name minio \
	# 自启动
	-d --restart=always \
	# 登录的用户名
  	-e "MINIO_ACCESS_KEY=minio" \
  	# 登录的密码
  	-e "MINIO_SECRET_KEY=minio" \
  	# 数据卷挂载
  	-v /data/docker/minio/data:/data \
	-v /data/docker/minio/config:/root/.minio \
	# 指定Minio版本
  	minio/minio:RELEASE.2021-08-05T22-01-19Z server /data \
  	# minio默认启动是动态端口,设置固定端口
	--address ':9000' --console-address ':9001'

    * */
